{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Out-of-Core Learning\n",
    "\n",
    "author: Jacob Schreiber <br>\n",
    "contact: jmschreiber91@gmail.com <br>\n",
    "\n",
    "Out-of-core learning refers to the process of training a model on an amount of data that cannot fit in memory. There are several approaches that can be described as out-of-core, but here we refer to the ability to derive exact updates to a model from a massive data set, despite not being able to fit the entire thing in memory.\n",
    "\n",
    "This out-of-core learning approach is implemented for all of pomegranate's models using two methods. The first is a `summarize` method that will take in a batch of data and reduce it down to additive sufficient statistics. Because these summaries are additive, after the first call, these summaries are added to the previously stored summaries. Once the entire data set has been seen, the stored sufficient statistics will be identical to those that would have been derived if the entire data set had been seen at once. The second method is the `from_summaries` method, which uses the stored sufficient statistics to derive parameter updates for the model.\n",
    "\n",
    "A common solution to having too much data is to randomly select an amount of data that does fit in memory to use in the place of the full data set. While simple to implement, this approach is likely to yield lower performance models because it is exposed to less data. However, by using out-of-core learning, on can train their models on a massive amount of data without being limited by the amount of memory their computer has."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Tue Nov 27 2018 \n",
      "\n",
      "numpy 1.15.1\n",
      "scipy 1.1.0\n",
      "pomegranate 0.10.0\n",
      "\n",
      "compiler   : GCC 7.2.0\n",
      "system     : Linux\n",
      "release    : 4.15.0-39-generic\n",
      "machine    : x86_64\n",
      "processor  : x86_64\n",
      "CPU cores  : 4\n",
      "interpreter: 64bit\n"
     ]
    }
   ],
   "source": [
    "%matplotlib inline\n",
    "import time\n",
    "import pandas\n",
    "import random\n",
    "import numpy\n",
    "import matplotlib.pyplot as plt\n",
    "import seaborn; seaborn.set_style('whitegrid')\n",
    "import itertools\n",
    "\n",
    "from pomegranate import *\n",
    "\n",
    "random.seed(0)\n",
    "numpy.random.seed(0)\n",
    "numpy.set_printoptions(suppress=True)\n",
    "\n",
    "%load_ext watermark\n",
    "%watermark -m -n -p numpy,scipy,pomegranate"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 1. Training a Probability Distribution\n",
    "\n",
    "Let's start off simple with training a multivariate Gaussian distribution in an out-of-core manner. First, we'll generate some random data."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "X = numpy.random.normal([5, 7], [1.5, 0.4], size=(1000, 2))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Then we can make a blank distribution with 2 dimensions. This is equivalent to filling in the mean and standard deviation with dummy values that will be overwritten, and don't effect the calculation."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "{\n",
       "    \"frozen\" :false,\n",
       "    \"class\" :\"Distribution\",\n",
       "    \"parameters\" :[\n",
       "        [\n",
       "            0.0,\n",
       "            0.0\n",
       "        ],\n",
       "        [\n",
       "            [\n",
       "                1.0,\n",
       "                0.0\n",
       "            ],\n",
       "            [\n",
       "                0.0,\n",
       "                1.0\n",
       "            ]\n",
       "        ]\n",
       "    ],\n",
       "    \"name\" :\"MultivariateGaussianDistribution\"\n",
       "}"
      ]
     },
     "execution_count": 3,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "d1 = MultivariateGaussianDistribution.blank(2)\n",
    "d1"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Now let's summarize through a few batches of data."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "d1.summarize(X[:250])\n",
    "d1.summarize(X[250:500])\n",
    "d1.summarize(X[500:750])\n",
    "d1.summarize(X[750:])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "[]"
      ]
     },
     "execution_count": 5,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "d1.summaries"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Now that we've seen the entire data set let's use the `from_summaries` method to update the parameters."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "{\n",
       "    \"frozen\" :false,\n",
       "    \"class\" :\"Distribution\",\n",
       "    \"parameters\" :[\n",
       "        [\n",
       "            4.967395273422254,\n",
       "            6.996038686884455\n",
       "        ],\n",
       "        [\n",
       "            [\n",
       "                2.1362097536595757,\n",
       "                -0.007992485878115985\n",
       "            ],\n",
       "            [\n",
       "                -0.007992485878115985,\n",
       "                0.15420875108571636\n",
       "            ]\n",
       "        ]\n",
       "    ],\n",
       "    \"name\" :\"MultivariateGaussianDistribution\"\n",
       "}"
      ]
     },
     "execution_count": 6,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "d1.from_summaries()\n",
    "d1"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "And what do we get if we learn directly from the data?"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "{\n",
       "    \"frozen\" :false,\n",
       "    \"class\" :\"Distribution\",\n",
       "    \"parameters\" :[\n",
       "        [\n",
       "            4.967395273422257,\n",
       "            6.996038686884458\n",
       "        ],\n",
       "        [\n",
       "            [\n",
       "                2.136209753659561,\n",
       "                -0.007992485878137813\n",
       "            ],\n",
       "            [\n",
       "                -0.007992485878137813,\n",
       "                0.1542087510856727\n",
       "            ]\n",
       "        ]\n",
       "    ],\n",
       "    \"name\" :\"MultivariateGaussianDistribution\"\n",
       "}"
      ]
     },
     "execution_count": 7,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "MultivariateGaussianDistribution.from_samples(X)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The exact same model."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 2. Training a Mixture Model"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "This summarization option enables a variety of different training strategies that can be written by hand. This notebook focuses on out-of-core learning, so let's make a data set and \"read it in\" one batch at a time to train a mixture model with a custom training function. We'll make another data set here, but one could easily have a function that read through some number of lines in a CSV, or loaded up a chunk from a numpy memory map, or whatever other massive data store you had."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "X = numpy.concatenate([numpy.random.normal(0, 1, size=(5000, 10)), numpy.random.normal(1, 1, size=(7500, 10))])\n",
    "n = X.shape[0]\n",
    "\n",
    "idx = numpy.arange(n)\n",
    "numpy.random.shuffle(idx)\n",
    "\n",
    "X = X[idx]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "First we have to initialize our model. We can do that either by hand to some value we think is good, or by fitting to the first chunk of data, anticipating that it will be a decent representation of the remainder. We can also calculate the log probability of the data set now to see how much we improved. "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "# First we initialize our model on some small chunk of data.\n",
    "model = GeneralMixtureModel.from_samples(MultivariateGaussianDistribution, 2, X[:200], max_iterations=1, init='first-k')\n",
    "\n",
    "# The base performance on the data set.\n",
    "base_logp = model.log_probability(X).sum()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "2b14bbabfb784fb2958f59567f60c26c",
       "version_major": 2,
       "version_minor": 0
      },
      "text/plain": [
       "HBox(children=(IntProgress(value=0, max=5), HTML(value=u'')))"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\n"
     ]
    }
   ],
   "source": [
    "from tqdm import tqdm_notebook as tqdm\n",
    "\n",
    "# Now we write our own iterator. This outer loop will be the number of times we iterate---hard coded to 5 in this case.\n",
    "for iteration in tqdm(range(5)):\n",
    "\n",
    "    # This internal loop goes over chunks from the data set. We're just loading chunks of a fixed size iteratively\n",
    "    # until we've seen the entire data set.\n",
    "    for i in range(10):\n",
    "        model.summarize(X[i * (n // 10):(i+1) * (n //10)])\n",
    "    \n",
    "    # Now we've seen the entire data set and summarized it. We can update the parameters now.\n",
    "    model.from_summaries()    "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "How we does did our model do on the data originally, and how well does it do now?"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "(-188806.57618011418, -184074.12225611554)"
      ]
     },
     "execution_count": 11,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "base_logp, model.log_probability(X).sum()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Looks like a decent improvement.\n",
    "\n",
    "Now, let's compare to having fit our model to the entire loaded data set for five epochs."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "(-188806.57618011418, -184074.12225611557)"
      ]
     },
     "execution_count": 12,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "model = GeneralMixtureModel.from_samples(MultivariateGaussianDistribution, 2, X[:200], max_iterations=1, init='first-k')\n",
    "base_logp = model.log_probability(X).sum()\n",
    "\n",
    "model.fit(X, max_iterations=5)\n",
    "base_logp, model.log_probability(X).sum()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Looks like the exact same values.\n",
    "\n",
    "You may ask why we bothered to write a summarization function for data that did fit in memory. The purpose here was entirely illustrative. Our function that use the summarize method would scale to any amount of data that could be loaded in batches, whereas the fit function can only scale to the amount of data that can fit in memory. However, they yield identical answers at the end, suggesting that if one wanted to scale to massive data sets but still get the same performance, this summarize function is the way to go."
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 2",
   "language": "python",
   "name": "python2"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 2
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython2",
   "version": "2.7.14"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
